<?php

declare(strict_types=1);

namespace Zlf\AppValidate;

use Closure;
use Zlf\AppException\Exception\ValidateException;
use Zlf\AppValidate\Rule\{AcceptedValidate,
    ArrayValidate,
    DateFormatValidate,
    DatetimeValidate,
    DateValidate,
    EmailValidate,
    ExcludeInValidate,
    IdentityNumberValidate,
    IntValidate,
    InValidate,
    IpValidate,
    JsonValidate,
    ListIntersectValidate,
    ListValidate,
    MatchValidate,
    MoneyValidate,
    MultiSelectorValidate,
    NumberValidate,
    PhoneValidate,
    PointValidate,
    QqValidate,
    RequiredChooseValidate,
    RequiredValidate,
    RuleAbstract,
    StringValidate,
    UrlValidate,
    ViolateValidate,
    ZipcodeValidate
};
use Zlf\AppValidate\Traits\ValidateTraits;
use Zlf\Unit\{
    Arr, Str
};

class Validate
{

    use ValidateTraits;


    /**
     * 没有某字段时是否跳过验证，对于required无效
     * @var bool
     */
    protected $noSkipping = true;


    /**
     * 空值是否跳过验证? 空值含义为empty($value)===true 对required,回调函数,自定义函数无效
     * @var bool
     */
    protected $emptySkip = true;


    /**
     * 结果中需要排除的数据
     * @var array
     */
    protected $exclude = [];

    /**
     * 原始数据
     * @var array
     */
    private $_raw = [];


    /**
     * 安全数据
     * @var array
     */
    private $_safe = [];


    /**
     * 验证规则
     * @var array
     */
    private $_rules = [];


    /**
     * 仅验证的字段,优先级大于场景
     * @var array
     */
    private $_fields = [];


    /**
     * 设置属性名称
     * @var array
     */
    private $_labels = [];


    /**
     * @var array 错误信息
     */
    private $_errors = [];


    /**
     * 设置验证场景
     * @var string
     */
    private $_scene = '';


    /**
     * 映射验证器,仅支持类验证器映射
     */
    public function verifyMapping(): array
    {
        return [
            Type::REQUIRED => RequiredValidate::class,//必填验证器
            Type::REQUIRED_CHOOSE => RequiredChooseValidate::class,//必选验证器
            Type::STRING => StringValidate::class,//字符串验证器
            Type::IN => InValidate::class,//in验证器
            Type::EXCLUDE_IN => ExcludeInValidate::class,//禁选指定项验证器
            Type::MULTI_SELECTOR => MultiSelectorValidate::class,//禁选指定项验证器
            Type::IDENTITY_NUMBER => IdentityNumberValidate::class,//身份证号码
            Type::URL => UrlValidate::class,//url链接验证器
            Type::POINT => PointValidate::class,//位置验证器
            Type::EMAIL => EmailValidate::class,//邮箱验证器
            Type::IP => IpValidate::class,//IP验证器
            Type::PHONE => PhoneValidate::class,//手机号验证器
            Type::MOBILE => PhoneValidate::class,//手机号验证器
            Type::LIST => ListValidate::class,//列表验证器
            Type::ARRAY => ArrayValidate::class,//数组验证器
            Type::MATCH => MatchValidate::class,//正则验证器
            Type::NUMBER => NumberValidate::class,//数字验证器
            Type::INT => IntValidate::class,//整数验证器
            Type::MONEY => MoneyValidate::class,//金额验证器
            Type::QQ => QqValidate::class,//QQ验证器
            Type::DATETIME => DatetimeValidate::class,//时间日期
            Type::DATE => DateValidate::class,//时间日期
            Type::DATE_FORMAT => DateFormatValidate::class,//时间格式验证
            Type::LIST_INTERSECT => ListIntersectValidate::class,//列表交集
            Type::JSON => JsonValidate::class,//JSON数据验器
            Type::ACCEPTED => AcceptedValidate::class,//确认验证器
            Type::ZIPCODE => ZipcodeValidate::class,//邮政编号验证
            Type::VIOLATE => ViolateValidate::class//违禁词检测
        ];
    }

    /**
     * 设置验证数据
     * @param $data
     * @return $this
     */
    public function setData($data): Validate
    {
        $this->_raw = $data;
        return $this;
    }


    /**
     * 获取原始数据
     * @return array
     */
    public function getData(): array
    {
        return $this->_raw;
    }


    /**
     * 设置字段标签名称
     * @param array $labels
     * @return $this
     */
    public function setLabels(array $labels): Validate
    {
        $this->_labels = $labels;
        return $this;
    }


    /**
     * 获取字段名称
     * @return array
     */
    public function getLabels(): array
    {
        return $this->_labels;
    }


    /**
     * 设置结果中要排除的字段
     * @param array $exclude
     * @return $this
     */
    public function setExclude(array $exclude): Validate
    {
        $this->exclude = $exclude;
        return $this;
    }


    /**
     * 获取结果中要排除的数据字段
     * @return array
     */
    public function getExclude(): array
    {
        return $this->exclude;
    }


    /**
     * 设置验证规则
     * @param array $rules
     * @return Validate
     */
    public function setRules(array $rules): Validate
    {
        $this->_rules = $rules;
        return $this;
    }


    /**
     * 获取验证规则
     * @return array
     */
    public function getRules(): array
    {
        return $this->_rules;
    }


    /**
     * 不存在的字段是否跳过
     * @param bool $emptySkip
     * @return $this
     */
    public function setEmptySkip(bool $emptySkip): Validate
    {
        $this->emptySkip = $emptySkip;
        return $this;
    }


    /**
     * 获取不存在的字段是否跳过
     * @return bool
     */
    public function getEmptySkip(): bool
    {
        return $this->emptySkip;
    }


    /**
     * 设置空值是否跳过
     * @param bool $noSkipping
     * @return $this
     */
    public function setNoSkipping(bool $noSkipping): Validate
    {
        $this->noSkipping = $noSkipping;
        return $this;
    }


    /**
     * 获取空值是否跳过
     * @return bool
     */
    public function getNoSkipping(): bool
    {
        return $this->noSkipping;
    }


    /**
     * 设置要验证的字段
     * @param array $field
     * @return $this
     */
    public function setFields(array $field): Validate
    {
        $this->_fields = $field;
        return $this;
    }


    /**
     * 获取要验证的值
     * @return array
     */
    public function getFields(): array
    {
        return $this->_fields;
    }

    /**
     * 设置验证场景
     * @param string $scene
     * @return $this
     */
    public function setScene(string $scene): Validate
    {
        $this->_scene = $scene;
        return $this;
    }


    /**
     * 获取验证场景
     * @return string|null
     */
    public function getScene()
    {
        return $this->_scene;
    }


    /**
     * 验证条件
     * @param string $find
     * @param array $value
     * @return bool
     */
    public function where(string $find, array $data): bool
    {
        return true;
    }

    /**
     * 获取验证规则
     * @throws ValidateException
     */
    private function getValidateRules(): array
    {
        $rules = [];
        foreach ($this->getRules() as $item) {
            if (!is_array($item)) throw new ValidateException('验证规则错误');
            if (empty($item[0])) throw new ValidateException('验证规则不完整');
            $find = [];
            if (is_string($item[0])) {
                $find[] = $item[0];
            } elseif (is_array($item[0]) && Arr::type($item[0]) === 'list') {
                $find = $item[0];
            } else {
                throw new ValidateException('数据key设置错误');
            }
            foreach ($find as $rowFind) {
                if ($this->needVerificationFind($rowFind, $item)) {
                    $rowRules = $item;
                    $rowRules[0] = $rowFind;
                    if (!isset($rowRules['emptySkip'])) {
                        $rowRules['emptySkip'] = $this->emptySkip;
                    }
                    if (!isset($rowRules['noSkipping'])) {
                        $rowRules['noSkipping'] = $this->noSkipping;
                    }
                    if ($rowRules[1] === Type::REQUIRED) {
                        $rowRules['emptySkip'] = false;
                        unset($rowRules['noSkipping']);
                    }
                    $rules[] = $rowRules;
                }
            }
        }
        return $rules;
    }


    /**
     * 判断此是否需要验此规则
     * @param string $find
     * @param array $rule
     * @return bool
     */
    private function needVerificationFind(string $find, array $rule): bool
    {
        if (count($this->_fields) > 0) {//字段验证模式
            return in_array($find, $this->_fields);
        } else if ($this->_scene) {//场景验证模式
            if (isset($rule['on']) && is_array($rule['on'])) {
                return in_array($this->_scene, $rule['on']);
            }
            return false;
        }
        return true;
    }


    /**
     * 获取验证状态
     * @return bool
     * @author 竹林风@875384189 2022/6/2 11:55
     */
    public function isFail(): bool
    {
        return count($this->_errors) > 0;
    }


    /**
     * 获取错误信息
     * @author 竹林风@875384189 2022/6/2 11:50
     */
    public function getErrors(): array
    {
        return $this->_errors;
    }

    /**
     * 获取错误信息列表
     * @author 竹林风@875384189 2022/6/2 11:50
     */
    public function getErrorList(): array
    {
        $list = [];
        if (count($this->_errors) > 0) {
            foreach ($this->_errors as $error) {
                if (is_string($error)) {
                    $list[] = $error;
                } elseif (is_array($error)) {
                    $list = array_merge($list, $error);
                }
            }
        }
        return array_unique($list);
    }


    /**
     * 获取第一条错误信息
     * @author 竹林风@875384189 2022/6/2 11:46
     */
    public function firstError(): string
    {
        $error = $this->getErrorList();
        if (count($error) > 0) {
            return Arr::firstValue($error);
        }
        return '';
    }


    /**
     * 获取第最后一条错误信息
     * @author 竹林风@875384189 2022/6/2 11:46
     */
    public function finalError(): string
    {
        $error = $this->getErrorList();
        if (count($error) > 0) {
            return Arr::finalValue($error);
        }
        return '';
    }


    /**
     * 添加错误信息
     */
    public function addError(string $find, string $error)
    {
        if (!isset($this->_errors[$find])) {
            $this->_errors[$find] = [];
        }
        $label = $this->getLabels()[$find] ?? $find;
        $this->_errors[$find][] = str_replace('{label}', strval($label), $error);
    }


    /**
     * 判断某字段是否有错误信息
     * @param string $find
     * @return bool
     */
    public function hasError(string $find): bool
    {
        return isset($this->_errors[$find]) && count($this->_errors[$find]) > 0;
    }


    /**
     * 添加错误信息
     */
    public function addErrors($find, array $errors)
    {
        foreach ($errors as $error) {
            $this->addError($find, $error);
        }
    }


    /**
     * 设置安全验证的数据
     * @param string $find
     * @param mixed $value
     * @return Validate
     */
    public function setSafe($find, $value): Validate
    {
        if (count($this->exclude) > 0 && in_array($find, $this->exclude)) {
            return $this;
        }
        $this->_safe[$find] = $value;
        return $this;
    }


    /**
     * 获取安全数据
     * @param false $all
     * @return array
     */
    public function getSafeData(bool $all = false): array
    {
        if ($this->isFail()) return [];
        $fields = $this->getFields();//指定了只要哪些字段
        if (count($fields) > 0 && $all === false) {
            $safe = [];
            foreach ($fields as $key) {
                if (isset($this->_safe[$key])) {
                    $safe[$key] = $this->_safe[$key];
                }
            }
            return $safe;
        }
        return $this->_safe;
    }


    /**
     * 获取指定安全数据
     * @param $key
     * @return mixed
     */
    public function getSafe($key)
    {
        $safe = $this->getSafeData();
        return $safe[$key] ?? null;
    }


    /**
     * 验证前事件
     * @param array $data
     * @return array
     */
    protected function beforeValidate(array $data): array
    {
        return $data;
    }


    /**
     * 验证后事件
     * @param array $data
     * @param array $safe
     * @return array
     */
    protected function afterValidate(array $data, array $safe): array
    {
        return $safe;
    }


    /**
     * 开始验证
     * @return $this
     * @throws ValidateException
     */
    public function validate()
    {
        $rules = $this->getValidateRules();//获取验证规则
        $data = $this->beforeValidate($this->getData());//待验证的数据
        $labels = $this->getLabels();//获取数据名称
        foreach ($rules as $rule) {
            //判断是否需要验证
            if ($this->where($rule[0], $data) === false) {
                continue;
            }
            if (isset($data[$rule[0]])) {//如果值存在
                $value = $data[$rule[0]];
            } else {//如果不存在这个数据字段
                if ($rule['emptySkip']) continue;//允许不存在的字段跳过验证
                $value = null;
            }
            //如果加了默认值且当前值为空就赋值默认值
            if (isset($rule['default']) && empty($value)) {
                $value = $rule['default'];
            }

            //安全数据之既然通过验证
            if ($rule[1] === Type::SAFE) {//视为安全数据
                if (!is_null($value)) {
                    $this->setSafe($rule[0], $value);
                }
                continue;
            }
            if (gettype($rule[1]) === 'string') {//验证规则为内置或方法名
                $verify = $this->verifyMapping();//获取内置规则映射
                $ruleClass = $verify[$rule[1]] ?? null;
                if ($ruleClass) {//内置验证规则
                    /**
                     * @var RuleAbstract $validate
                     */
                    $validate = new $ruleClass;
                    if (!($validate instanceof RuleAbstract)) {
                        throw new ValidateException('验证规则无效');
                    }
                    $methods = get_class_methods($validate);
                    foreach ($rule as $key => $conf) {
                        $method = Str::doubleToHump('set_' . $key);
                        if (in_array($method, $methods)) {
                            call_user_func([$validate, $method], $conf);
                        }
                    }
                    if ($validate->validate($data[$rule[0]] ?? null, $labels[$rule[0]] ?? $rule[0]) === true) {
                        $this->setSafe($rule[0], $validate->getValue());
                    } else {
                        $this->addErrors($rule[0], $validate->getErrors());
                    }
                    continue;
                } elseif (method_exists($this, $rule[1])) {//class的方法验证器
                    $status = call_user_func([$this, $rule[1]], $value, $rule[0], $data);
                    if (is_null($status)) {//如果返回null说明没通过验证
                        if (!$this->hasError($rule[0])) {
                            $this->addError($rule[0], $rule['error'] ?? '{label}未通过验证');
                        }
                    } else {
                        //设置安全数据
                        $this->setSafe($rule[0], $status);
                    }
                    continue;
                } elseif (method_exists($this, $rule[1] . 'Validate')) {//内置规则
                    $status = call_user_func([$this, $rule[1] . 'Validate'], $value, $rule[0], $data, $rule);
                    if (is_null($status)) {
                        if (!$this->hasError($rule[0])) {
                            $this->addError($rule[0], $rule['error'] ?? '{label}未通过验证');
                        }
                    } else {
                        $this->setSafe($rule[0], $status);
                    }
                    continue;
                }
            } else if (gettype($rule[1]) === 'object' && $rule[1] instanceof Closure) {//回调函验证方式
                $status = call_user_func($rule[1], $value, $rule[0], $data);
                if (is_null($status)) {
                    if (!$this->hasError($rule[0])) {
                        $this->addError($rule[0], $rule['error'] ?? '{label}未通过验证');
                    }
                } else {
                    $this->setSafe($rule[0], $status);
                }
                continue;
            }
            throw new ValidateException("验证规则{$rule[1]}无效");
        }
        $this->_safe = $this->afterValidate($this->_raw, $this->_safe);
        return $this;
    }
}